﻿/*
Copyright (c) 2010 Lance Ward
This software is licensed under the MIT license, except where otherwise noted.
For complete license details, refer to license.txt
*/

using System;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;

namespace ADNCanvas
{
	public class Viewport : Control
	{

		private Rectangle doc = new Rectangle(0, 0, 1, 1);

		private float zoom = .25f;
		private float xOffset;
		private float yOffset;
		private Matrix inMatrix = new Matrix();		//for translating inbound points
		private Matrix outMatrix = new Matrix();	//for translating outbound points (display)

		private HScrollBar hScrollBar;
		private VScrollBar vScrollBar;
		private const float bigChange = .20f;
		private const float smallChange = .05f;

		private bool forceRedraw = false;

		#region Initialization

		public Viewport()
		{
			InitializeComponent();
		}

		private void InitializeComponent()
		{
			this.BackColor = Color.White;
			this.MouseWheel += new MouseEventHandler(Viewport_MouseWheel);		//no override, for some reason

			this.hScrollBar = new HScrollBar();
			this.vScrollBar = new VScrollBar();

			this.SuspendLayout();

			this.hScrollBar.Location = new Point(0, 300);
			this.hScrollBar.Name = "hScrollbar";
			this.hScrollBar.Size = new Size(500, 17);
			this.hScrollBar.TabIndex = 0;
			this.hScrollBar.Visible = true;
			hScrollBar.Scroll += new ScrollEventHandler(hScrollBar_Scroll);

			this.vScrollBar.Location = new Point(500, 0);
			this.vScrollBar.Name = "vScrollbar";
			this.vScrollBar.Size = new Size(17, 300);
			this.vScrollBar.TabIndex = 0;
			this.vScrollBar.Visible = true;
			vScrollBar.Scroll += new ScrollEventHandler(vScrollBar_Scroll);

			this.Controls.Add(this.vScrollBar);
			this.Controls.Add(this.hScrollBar);

			//determine, based on docsize and centerpoint whether scrollbars are needed.
			this.ResumeLayout(false);

			//use default doc resolution and a zoom of 25% to create the initial numbers

			SetZoom(1.0f);
		}

		#endregion

		#region Properties

		//overriddent to prevent usage
		public override Point AutoScrollOffset
		{
			get { return new Point(0, 0); }
			set { base.AutoScrollOffset = new Point(0, 0); }
		}

        public float XOffset
        {
            get { return xOffset; }
        }

        public float YOffset
        {
            get { return yOffset; }
        }

		public Size Document
		{
			get { return new Size(doc.Width, doc.Height); }
			set { SetDocResolution(value); }
		}

		public RectangleF DocumentBounds
		{
			get { return TranslateOut(doc); }
		}

		public float Zoom
		{
			get { return zoom; }
			set { SetZoom(value); }
		}

		public Rectangle View	//the current drawable area of the control
		{
			get { return GetViewRect(); }
		}

		public Matrix InMatrix
		{
			get { return inMatrix; }
		}

		public Matrix OutMatrix
		{
			get { return outMatrix; }
		}

		public bool ForceRedraw
		{
			get { return forceRedraw; }
			set { forceRedraw = value; }
		}

		#endregion

		#region Zoom
		//Position tells us where the canvas should be centered
		public void SetZoom(float zoomLevel)
		{
			zoom = zoomLevel;

			Reset();

			forceRedraw = true;
			this.Invalidate();
		}

		//zoom so that this rectangle fills the screen, used for zoom to shape as well as the zoom tool drawing rectangle

		public void SetZoom(Rectangle zoomRect, Rectangle screenRect)
		{
			float xZoom = ((float)screenRect.Width / (float)(doc.Width + 100.0f));
			float yZoom = ((float)screenRect.Height / (float)(doc.Height + 100.0f));

			SetZoom(Math.Min(xZoom, yZoom));
		}

		//zoom to fit the current document in the window

		public void SetZoom(Rectangle screenRect)
		{
			float xZoom = ((float)screenRect.Width / (float)(doc.Width + 100.0f));
			float yZoom = ((float)screenRect.Height / (float)(doc.Height + 100.0f));

			SetZoom(Math.Min(xZoom, yZoom));
		}

		#endregion

		#region Translate
		//Inbound translations, for storage

		public Rectangle TranslateIn(Rectangle rect)
		{
			Point p = Translate(rect.Location, inMatrix);

			rect.X = p.X;
			rect.Y = p.Y;

			return rect;
		}

		public RectangleF TranslateIn(RectangleF rect)
		{
			PointF p = Translate(rect.Location, inMatrix);

			rect.X = p.X;
			rect.Y = p.Y;

			return rect;
		}

		public Point TranslateIn(Point point)
		{
			return Translate(point, inMatrix);
		}

		public PointF TranslateIn(PointF point)
		{
			return Translate(point, inMatrix);
		}

		//Outbound translations (for display)

		public Rectangle TranslateOut(Rectangle rect)
		{
			Point p = Translate(rect.Location, outMatrix);

			rect.X = p.X;
			rect.Y = p.Y;

			return rect;
		}

		public RectangleF TranslateOut(RectangleF rect)
		{
			PointF p = Translate(rect.Location, outMatrix);

			rect.X = p.X;
			rect.Y = p.Y;

			return rect;
		}

		public Point TranslateOut(Point point)
		{
			return Translate(point, outMatrix);
		}

		public PointF TranslateOut(PointF point)
		{
			return Translate(point, outMatrix);
		}

		public float TranslateOut(float penSize)
		{
			//to be used only for translating pen sizes
			return penSize * zoom;
		}

		private PointF Translate(PointF point, Matrix matrix)
		{
			PointF[] p = new PointF[1] { point };
			matrix.TransformPoints(p);
			point.X = p[0].X;
			point.Y = p[0].Y;

			return point;
		}

		private Point Translate(Point point, Matrix matrix)
		{
			Point[] p = new Point[1] { point };
			matrix.TransformPoints(p);

			point.X = p[0].X;
			point.Y = p[0].Y;

			return point;
		}

		#endregion

		private Rectangle GetViewRect()
		{
			Rectangle rect = this.Bounds;
			rect.Width -= vScrollBar.Visible ? vScrollBar.Width : 0;
			rect.Height -= hScrollBar.Visible ? hScrollBar.Height : 0;

			return rect;
		}

		private void SetDocResolution(Size s)
		{
			doc.Height = s.Height;
			doc.Width = s.Width;

			Reset();
		}

		private void Reset()
		{
			SetScroll();
			SetOffset();
			SetMatrix();
		}

		//resets scroll based on current zoom, doc size and margins
		private void SetScroll()
		{
			hScrollBar.Maximum = (int)Math.Ceiling((double)(doc.Width * zoom / 2));
			hScrollBar.Minimum = -(int)Math.Ceiling((double)(doc.Width * zoom / 2));
			hScrollBar.LargeChange = (int)(hScrollBar.Maximum * bigChange);
			hScrollBar.SmallChange = (int)(hScrollBar.Maximum * smallChange);

			vScrollBar.Maximum = (int)Math.Ceiling((double)(doc.Height * zoom / 2));
			vScrollBar.Minimum = -(int)Math.Ceiling((double)(doc.Height * zoom / 2));
			vScrollBar.LargeChange = (int)(vScrollBar.Maximum * bigChange);
			vScrollBar.SmallChange = (int)(vScrollBar.Maximum * smallChange);
		}

		//when scroll values, zoom, doc size or view rect changes
		private void SetOffset()
		{
			Point center = GetCenterPoint(this.Bounds);

			RectangleF rect = new RectangleF(doc.X, doc.Y, doc.Width, doc.Height);
			rect.Width *= zoom;
			rect.Height *= zoom;

			PointF docCenter = GetCenterPoint(rect);
			xOffset = center.X - docCenter.X;
			yOffset = center.Y - docCenter.Y;

			//add scrollbar position
			xOffset += -hScrollBar.Value;
			yOffset += -vScrollBar.Value;
		}

		private void SetMatrix()
		{
			inMatrix.Reset();
			outMatrix.Reset();

			inMatrix.Translate(xOffset, yOffset);
			inMatrix.Scale(zoom, zoom, MatrixOrder.Prepend);
			inMatrix.Invert();

			outMatrix = inMatrix.Clone();
			outMatrix.Invert();
		}

		private Point GetCenterPoint(Rectangle rect)
		{
			Point p = new Point();
			p.X = (rect.X + rect.Width) / 2;
			p.Y = (rect.Y + rect.Height) / 2;

			return p;
		}

		private PointF GetCenterPoint(RectangleF rect)
		{
			PointF p = new PointF();
			p.X = (rect.X + rect.Width) / 2;
			p.Y = (rect.Y + rect.Height) / 2;

			return p;
		}

		#region Events

		protected override void OnPaint(PaintEventArgs e)
		{
			e.Graphics.Clear(Color.FromArgb(201,211,226));
			e.Graphics.Transform = outMatrix;

			float borderWidth = zoom > 0.0f ? 1.0f / zoom : 1.0f;
			RectangleF rect = new RectangleF(doc.X - borderWidth,
				doc.Y - borderWidth, doc.Width + borderWidth, doc.Height + borderWidth);
			e.Graphics.FillRectangle(new SolidBrush(Color.White), rect.X, rect.Y, rect.Width, rect.Height);
			e.Graphics.DrawRectangle(new Pen(Color.FromArgb(100, Color.Black), borderWidth), rect.X, rect.Y, rect.Width, rect.Height);
			//e.Graphics.FillRectangle(new SolidBrush(Color.White), doc.X, doc.Y, doc.Width, doc.Height);

			base.OnPaint(e);
		}

		void vScrollBar_Scroll(object sender, ScrollEventArgs e)
		{
			Reset();
			forceRedraw = true;
			this.Invalidate();
		}

		void hScrollBar_Scroll(object sender, ScrollEventArgs e)
		{
			Reset();
			forceRedraw = true;
			this.Invalidate();
		}

		protected override void OnResize(EventArgs e)
		{
			this.SuspendLayout();

			//reposition scrollbars to align with the outside edges of the control
			hScrollBar.Location = new Point(0, this.Height - hScrollBar.Height);
			hScrollBar.Size = new Size(this.Width - hScrollBar.Height, 17);

			vScrollBar.Location = new Point(this.Width - vScrollBar.Width, 0);
			vScrollBar.Size = new Size(17, this.Height - vScrollBar.Width);

			Reset();

			this.ResumeLayout(false);
			forceRedraw = true;
			this.Invalidate();
		}


		void Viewport_MouseWheel(object sender, MouseEventArgs e)
		{
			int movedLines = (-e.Delta * SystemInformation.MouseWheelScrollLines / 120) * vScrollBar.SmallChange;

			if (movedLines + vScrollBar.Value < vScrollBar.Maximum && movedLines + vScrollBar.Value> vScrollBar.Minimum)
			{
				vScrollBar.Value += movedLines;
				Reset();
				forceRedraw = true;
				this.Invalidate();
			}
		}

		#endregion
	}
}
